﻿//===================================================================================
// Copyright (c) Ryan Garrett.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================

using Caffeine.Core.Metadata;
using System;

namespace Caffeine.Core.ModelProperty
{
    internal sealed class ModelChildProperty<T> : 
        ModelProperty<T> where T : ModelBase, new()
    {
        #region Constructors

        public ModelChildProperty(ModelBase parent, String name, Nullable<ModelPropertyBehaviors> behavior, Boolean isWrapped)
            : base(parent, name, behavior, isWrapped)
        {
        }

        #endregion

        #region Public

        public override Boolean IsModelProperty
        {
            get
            {
                return true;
            }
        }

        public override String ToString()
        {
            String stringValue = null;

            if (Value != null)
            {
                stringValue = Value.ToString();
            }

            return stringValue;
        }

        #endregion

        #region Internal

        internal override ModelScalarPropertyMetadata GetMetadata()
        {
            ModelScalarPropertyMetadata metadata = base.GetMetadata();

            if (Value != null)
            {
                metadata.Value = Value.GetMetadata();
            }

            //If this property is committed, that means the Value and CommittedValue properties point to the
            //same reference. There's no need to represent the same object graph twice.
            if (IsDirty && CommittedValue != null)
            {
                metadata.CommittedValue = CommittedValue.GetMetadata();
            }

            return metadata;
        }

        internal override void MergeAdjacentProperty(ModelPropertyBase property)
        {
            ModelChildProperty<T> sourceProperty = property as ModelChildProperty<T>;

            if (Value != null && (Value.UniqueId == null || Value.UniqueId == Guid.Empty))
            {
                throw new InvalidOperationException("All models in merge must have their UniqueId property assigned");
            }

            if (sourceProperty.Value != null)
            {
                if (sourceProperty.Value.UniqueId == null || sourceProperty.Value.UniqueId == Guid.Empty)
                {
                    throw new InvalidOperationException("All models in merge must have their UniqueId property assigned");
                }

                if (Value != null)
                {
                    if (Value.UniqueId != sourceProperty.Value.UniqueId)
                    {
                        Value = Parent.ParentFactory.CreateModel(sourceProperty.Value.GetType()) as T;
                        Value.UniqueId = sourceProperty.Value.UniqueId;
                    }
                }
                else
                {
                    Value = Parent.ParentFactory.CreateModel(sourceProperty.Value.GetType()) as T;
                    Value.UniqueId = sourceProperty.Value.UniqueId;
                }
                
                Value.Merge(sourceProperty.Value);
            }
            else
            {
                Value = null;
            }
        }

        #endregion

        #region Protected

        protected override void OnBeforeCommit()
        {
            base.OnBeforeCommit();

            if (CommittedValue != null)
            {
                CommittedValue.IsRemovedFromParentProperty = false;
                CommittedValue.SetParent(null, null, false, false);
                CommittedValue.RaiseOnRemovedFromProperty(this);
            }
        }

        protected override void OnAfterCommit()
        {
            base.OnAfterCommit();

            if (CommittedValue != null)
            {
                CommittedValue.IsCommittedToParentProperty = true;
                CommittedValue.IsAssignedToParentProperty = false;
                CommittedValue.RaiseOnCommittedToProperty(this);
            }
        }

        protected override void ConfigureParentChildRelationships(T previousValue, T newValue)
        {
            base.ConfigureParentChildRelationships(previousValue, newValue);

            Boolean allowParentIsDirtyWithinChange      = !(newValue != null && newValue.IsParentDirtyWithin && previousValue   != null && previousValue.IsParentDirtyWithin);
            Boolean allowParentIsInvalidWithinChange    = !(newValue != null && newValue.IsParentInvalidWithin && previousValue != null && previousValue.IsParentInvalidWithin);

            if (previousValue != null)
            {
                if (previousValue != CommittedValue)
                {
                    previousValue.IsAssignedToParentProperty = false;
                    previousValue.SetParent(null, null, allowParentIsDirtyWithinChange, allowParentIsInvalidWithinChange);
                    previousValue.RaiseOnRemovedFromProperty(this);
                }
                else
                {
                    previousValue.IsRemovedFromParentProperty = true;
                    previousValue.IsCommittedToParentProperty = false;
                    previousValue.SignalRemovedFromParentProperty(allowParentIsDirtyWithinChange, allowParentIsInvalidWithinChange);
                    previousValue.RaiseOnMarkedForRemovalFromProperty(this);
                }
            }

            if (newValue != null)
            {
                if (newValue != CommittedValue)
                {
                    if (newValue.Parent != null)
                    {
                        throw new InvalidOperationException("Model already has a parent");
                    }

                    if (newValue == Parent)
                    {
                        throw new InvalidOperationException("A model cannot be its own child");
                    }

                    newValue.SetParent(Parent, this, allowParentIsDirtyWithinChange, allowParentIsInvalidWithinChange);
                    newValue.IsAssignedToParentProperty = true;
                    newValue.RaiseOnAssignedToProperty(this);
                }
                else
                {
                    newValue.IsCommittedToParentProperty = true;
                    newValue.IsRemovedFromParentProperty = false;
                    newValue.SignalRestoredToParentProperty(allowParentIsDirtyWithinChange, allowParentIsInvalidWithinChange);
                    newValue.RaiseOnCommittedToProperty(this);
                }
            }
        }

        protected override Boolean OnCompareValues(T currentValue, T newValue)
        {
            return Object.ReferenceEquals(currentValue, newValue) ||
                   (currentValue != null && newValue != null && currentValue.Equals(newValue));
        }

        protected override void OnApplyScalarPropertyMetadata(ModelScalarPropertyMetadata metadata, IModelFactory factory)
        {
            ModelMetadata modelMetadata = metadata.Value as ModelMetadata;

            if(modelMetadata != null)
            {
                InternalValue = factory.CreateModel<T>
                (
                    modelMetadata, 
                    this
                );
            }

            if (metadata.IsDirty)
            {
                modelMetadata = metadata.CommittedValue as ModelMetadata;

                if (modelMetadata != null)
                {
                    InternalCommittedValue = factory.CreateModel<T>
                    (
                        modelMetadata,
                        this
                    );
                }
            }
            else
            {
                InternalCommittedValue = InternalValue;
            }
        }

        #endregion
    }
}
